# -*- coding: utf-8 -*- 

## ------------------------------------------------------------
## Copyright 2010 Pierre-Antoine Delsart
## 
## This file is part of LeVin.
## 
## LeVin is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
## 
## LeVin is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
## 
## You should have received a copy of the GNU General Public License
## along with LeVin.  If not, see <http://www.gnu.org/licenses/>.
## ---------------------------------------------------------------
"""
This module defines a hieratchical description of wine areas.

The implementation is a bit complex and tries to provide the following
requirements :
 - Implement a hierarchy World -> Country -> Region (-> sub-region) -> wine appellation
 - areas have attributes (ex: cepages, sub-areas,...)
 - some of these attributes are inherited (ex: cepages), some not (sub-areas)
 - the hierarchy can be extended by users (modificiation of any attributes)
 - USer modifications (and only them) are saved (pickeld) at the end of each session
 - When LeVin is updated (more areas described), this should not affect user-data AND be available to user transparently

 To achieve this the implementation make use of the following.
  - hierarchy and inheritance of attributes is provided by classic
  class inheritance
  - Attributes are provided by creating class inheriting AttributeHolder and have the
  base Area class inhriting it. (See AttributeHolder desc)
  - All areas classes specific to a region (thus defined by LeVin, not the user) are singleton.
  the system-definded datas are class members only (thus are not pickled).
"""


from PythonQt import QtCore, QtGui, QtSvg

from ordereddict import OrderedDict
from qtcommon import *
import wineBase

wineAreas  =  sys.modules['wineAreas']

class AttributeHolder(object):
    """ A base class to define wrapper around attributes.

     each class defines one attribute, ex 'foo',
     - have a user dedicated member identified by '_user' (ex: attributes '_foo' and '_foo_user')
     - when a client class modifies the attribute, reacting to a user input,  'self._foo_user' is created and
     affected. Not 'cls._foo_user' (the later being here just as a placeholder, and never saved).
     - only 'self._foo_user' attributes are saved (pickled).

     This base class implements the common functionnalities :
      - creation of the '_user' attribute (AttributeHolder class must call buildUserAttr when needed)
      - check if the user attribute already exists

     Concrete implementations must implement an accessor function 'foo(..any args...)'
     """
    attname = ''
    attributeBuilder = None

    modified = False
    @classmethod
    def hasUserAttr(cls, self):
        aname = '_'+cls.attname+'_user'
        return not (getattr(self, aname) is getattr(cls, aname))

    @classmethod
    def attributeMemberName(cls):
        return '_'+cls.attname

    
    @classmethod
    def buildUserAttr(cls,self):
        if cls.hasUserAttr(self):
            return
        # create an attribute
        aname = cls.attributeMemberName()+'_user'
        setattr(self,aname, cls.attributeBuilder() )
        #print 'buildUserAttr ',cls,self , self.__class__,cls.attname, aname
        WorldArea.allAreas.append(self)
        
def attributeMaker(attname, attbuilder):
    """ Intended to be used as a class decorator for classes inheriting AttributeHolder.
    attname : str, the attribute name the class describes. a attname method and _attname member are genrated.
    attbuilder : a function taking 1 arg, returning a new attribute (the real attribute object, ex: a list, a dict,...)
    """
    def addAtt(cls):
        cls.attname = attname
        cls.attributeBuilder = staticmethod(attbuilder)
        #setattr(cls,attname, getattr(cls,attname+'_base' ) )
        setattr(cls,'_'+attname, attbuilder() )
        setattr(cls,'_'+attname+'_user', attbuilder() )        
        return cls
    return addAtt

# the function creating a new cepages store (here a list of list)
_newcepages = lambda  : [ list() for c in wineBase.colors ]

@attributeMaker('cepages', _newcepages)
class CepagesAtt(AttributeHolder):
    """AttributeHolder dedicated to cepages """
    inherited = True

    def cepages(self, color):
        return self._cepages[color] + self._cepages_user[color]
    def addCepages(self,color,c):
        CepagesAtt.buildUserAttr(self)
        self._cepages_user[color].append(c)


_newsubareas = lambda : OrderedDict()
@attributeMaker('subAreas',_newsubareas)
class SubAreaAtt(AttributeHolder):
    inherited = False

    def subAreas(self):
        return self._subAreas.values() + self._subAreas_user.values()

    def subAreasKeys(self):
        if SubAreaAtt.hasUserAttr(self):
            return self._subAreas.keys() + self._subAreas_user.keys()
        else:
            return self._subAreas.keys()
        
    def __getitem__(self,s):
        #print 'requesting subAreas ',s
        area = self._subAreas.get(s,None)
        if area is None:
            #print s,' not found in _subAreas '
            SubAreaAtt.buildUserAttr(self)
            area = self._subAreas_user.get(s,None)
        if area is None: # build it !
            #print s,' not found in _subAreas_user '            
            area = self.subAreaClass(name=s,parentArea=self)
            self._subAreas_user[s] = area
        return area

    
    def __setitem__(self,s, a):
        #print 'setitem', self
        SubAreaAtt.buildUserAttr(self)
        self._subAreas_user[s] = a


class AttMetaClass(type):
    """MetaClass for class having attributes """
    attributes = (CepagesAtt, SubAreaAtt)
    def __new__(metacls, name, bases, dct):
        #print 'AttMetaClass',name, bases
        klass = type.__new__(metacls, name, bases, dct)        
        for c in metacls.attributes:
            #c.setBaseAttr(klass)
            if not c.inherited:
                setattr(klass, c.attributeMemberName(), c.attributeBuilder() )
        #bases = AAInheriter.classtoadd +bases
        return klass

class Singleton(object):
    _instance = None
    _isSingleton = True
    def __new__(cls, *args, **kwargs):
        if cls._isSingleton:
            if not isinstance(cls._instance,cls): # make sure we're not returning parent's instance
                cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
            #print 'Creating Singleton ',cls, cls._instance
            #WorldArea.allAreas.append(cls._instance)
            return cls._instance
        
        # else  return a new instance.
        o  = super(Singleton, cls).__new__(cls)
        o.__dict__.update(kwargs)
        if not cls._isSingleton:
            #print 'Creating non single ',cls, o,kwargs            
            for att in AttMetaClass.attributes:
                att.buildUserAttr(o)
        # in any case, we keep a ref to the object so it is pickled :
        WorldArea._instance.allAreas.append(o)
        return o
   

class Area(Singleton, CepagesAtt, SubAreaAtt):
    """Base class for describing hierarchical wine regions.

    Area defines a tree structures with a parentArea and sub-areas (ex: a Country and its regions)
    """

    __metaclass__ = AttMetaClass

    fullId_str = None
    parentArea=None
    def __init__(self, name=None, parentArea=None, subAreas=[], id = None , cepages=None, appellations=[]):
        #self.cepages = self._getcepages
        if name is None:
            # a system defined class, it should have been initialized correctly.            
            return
        self.name = unicode(name)
        self.parentArea =parentArea
        self.item = None
        self.id = id
        if cepages:
            self._cepages = cepages
        

    def fullId(self):
        #print 'fullId ',self.name, self.id
        if self.fullId_str:
            return self.fullId_str
        base = []
        if self.parentArea != wineWorld:
            base.append( self.parentArea.fullId() )
        if self.id:
            base.append( self.id)        
        base = '_'.join( base )
        self.fullId_str = base.replace(' ','_')
        return self.fullId_str

    @classmethod
    def generateUniqueSubAreaClass(cls, classname=None, **args):
        if not classname:
            classname = cls.subAreaClass.__name__+str(hash(args['id']))
        newclass =  type(classname , (cls.subAreaClass,), args )
        newclass._isSingleton = True
        #print 'generateUniqueSubArea  ', cls, args, '   ==>',newclass
        addClassToModule(newclass)
        return newclass

    @classmethod
    def generateUniqueSubArea(cls, **args):
        a = cls.generateUniqueSubAreaClass(**args)()
        
        return a

    def doRecursively(self,f):
        f(self)
        for i in self.subAreas():
            i.doRecursively(f)

    def dump(self,i=0):
        print '-'*2*i+'>',self.name, self.__class__, self
        for a in self.subAreas():
            a.dump(i+1)

    def __getstate__(self):
        #print 'pickcling ',self , '  ',self.name ,self.__dict__
        return self.__dict__
        
    ## def __getnewargs__(self):
    ##     print '__getnewargs__',self
    ##     return (0,)
            

    

class CountryArea(Area):

    attributeInDB = 'country'

    _isSingleton = False

    def addRegions(self, *areas):
        for a in areas:
            if isinstance(a,str):
                __import__('data.'+a)
                datamodule = sys.modules['data.'+a]
                a = datamodule.createRegion()
                a.parentArea = self
            else: # must be a tuple (name, id, cepages)
                name, id, _cepages = a
                a = self.generateUniqueSubArea(name=unicode(name),id=id,_cepages=_cepages, classname=self.__class__.__name__+id)
            self._subAreas[a.name] = a
            a.parentArea = self


class RegionArea(Area):
    _isSingleton = False # generic classes are not singletons
    
    attributeInDB = 'region'

    nAppellationGroup = 0
    
    def appellations(self):
        al = []
        #print 'appelation for ',self.subAreas()
        for a in self.subAreas():
            a.fillAppellationList( al )
        return al

    def addAppellations(self, appList):
        #print 'addAppellations ',self, appList
        container = self
        for a in appList:
            if not isinstance( a, unicode):
                a.parentArea = self                
                self._subAreas[unicode(a.name)] = a                
                container = a
                continue
            app = container.generateUniqueSubArea(name=a,id=a, parentArea=container)
            container._subAreas[a] = app


    @classmethod
    def newgroup(cls, name, id=None,cepages=None):
        if not id:
            id=cls.name+str(cls.nAppellationGroup)
            cls.nAppellationGroup+=1
        tmpApp = cls.subAreaClass
        cls.subAreaClass = cls.appellationGroupClass
        g=cls.generateUniqueSubArea(name=unicode(name),id=id ,cepages=cepages )
        cls.subAreaClass = tmpApp
        #print '  newgroup ',cls,id,' generated  --> '        ,g
        return g


class _AppellationAreaMeths(object):
    def fillAppellationList(self, al):
        al.append( self.name )
class AppellationArea(_AppellationAreaMeths):
    _isSingleton = False # generic classes are not singletons
    lowerclass = object
    attributeInDB = 'appellation'


class _AppellationGroupMeths(object):
    def fillAppellationList(self, al):
        #print 'fillAppellationList group : ',self.subAreas()
        for a in self.subAreas():
            al.append( a.name )
class AppellationGroup(_AppellationGroupMeths):
    _isSingleton = False # generic classes are not singletons
    lowerclass = AppellationArea
    attributeInDB = 'appellation'



    
def regionClassGenerator(id, name, parentCountry):
    region = type(id+'Area', (RegionArea,parentCountry), {} ) # the region derived class
    region.id = id
    region.name = unicode(name)
    region._isSingleton = True
    
    appgroup = type(id+'Group', (_AppellationGroupMeths,region), {} ) # this region appellation group class
    appgroup._isSingleton = False
    
    app = type(id+'Appellation', (_AppellationAreaMeths,appgroup,), {} ) # this region appellation class
    app._isSingleton = False

    # define the hierarchy in this region
    region.subAreaClass = app # this is the default, regions may not have sub-groups
    #region.parentArea = parentCountry()
    region.appellationGroupClass = appgroup
    appgroup.subAreaClass = app


    addClassToModule(region)
    addClassToModule(app)
    addClassToModule(appgroup)
    #appgroup.parentArea = region
    #app.parentArea = region

    return region, app, appgroup, region.newgroup



class WorldArea(Area):
    subAreaClass = CountryArea
    topTreeItem = None
    allAreas = []
    def createTreeItem(self,bott):
        return WorldArea.topTreeItem
    def setupTreeItem(self,bott):
        pass
    def setStyle(self):
        pass

    def update(self, d):
        for c,regions in d.iteritems() :
            area_c = self[unicode( c )]  # we dont store QString object, convert to unicode !!
            area_c.id = regions[0]
            for r in regions[1]:
                area_c[unicode(r[0]) ].id = r[1]
        #print wineWorld['France'] , wineWorld['France'].id

    def makeWritable(self):
        for i in self.subAreas():
            i.makeWritable()
    def fullId(self):
        return ''

    def addCountry(self, cname):
        if isinstance(cname,str):
            c = self.generateUniqueSubArea(cname,self)
            WorldArea._subAreas[cname] = c
        else:
            # assume cname is a class
            c = cname()
            WorldArea._subAreas[unicode(cname.name)] = c
        c.parentArea = self
        return c
        
    def initCountries(self, *areas):
        #print areas
        for a in areas:
            if isinstance(a,str):
                __import__('data.'+a)
                datamodule = sys.modules['data.'+a]
                a = datamodule.createRegion()
                        
            WorldArea._subAreas[a.name] = a
            a.parentArea = self


wineWorld = WorldArea("world",None)
wineWorld.allAreas = WorldArea.allAreas


# define the hierarchy here, once all classes are defined :
CountryArea.subAreaClass = RegionArea
RegionArea.subAreaClass  = AppellationArea

wineColors = None
def setupWineColors():
    """This allow to delay initialisation of colors after translation system is on"""
    global wineColors
    wineColors= [
        tr("Red" ),
        tr("White" ),
        tr("Pink" ),
        ]


def addClassToModule(klass):
    setattr(wineAreas, klass.__name__, klass)
def wineWorldToDic():
    d = {}
    for c,regions in wineWorld.subAreas.iteritems():
        d[c] = [ r for r in regions.subAreas.keys() ]
    return d

def save_wineWorld(fname):
    import cPickle as pk
    #import pickle as pk
    wineWorld.makeWritable()
    pk.dump( wineWorld , open(fname, 'w'), -1 )
def load_wineWorld(fname):    
    global wineWorld
    print 'Loading wineWorld.  ',wineWorld
    import cPickle as pk
    wineWorld = pk.load( open(fname) )
    #print 'Loading wineWorld.  ',wineWorld
    #print wineWorld.subAreas
                  

def default_wineWorld():
    global wineWorld
    print 'default_wineWorld'    
    ## for k,v in wineWorld.subAreas.iteritems():
    ##     v.id = k


def addIdName(id,name):
    def adder(cls):
        cls.id = id
        cls.name = name
        return cls
    return adder

def createCountryClass(id,name):
    """One-liner to create a country class inherinting CountryArea """
    cls = type(id+'Area', (CountryArea,), {'id':id,'name':unicode(name)} ) # the region derived class
    cls._isSingleton = True
    wineWorld.addCountry(cls)
    return cls

FranceArea      =  createCountryClass("France",tr("France"))
ItalyArea       =  createCountryClass("Italy",tr("Italy"))
SpainArea       =  createCountryClass("Spain",tr("Spain"))
PortugalArea    =  createCountryClass("Portugal",tr("Portugal"))
GermanyArea     =  createCountryClass("Germany",tr("Germany"))
GreeceArea      =  createCountryClass("Greece",tr("Greece"))
USAArea         =  createCountryClass("USA",tr("USA"))
SouthAfricaArea =  createCountryClass("SouthAfrica",tr("SouthAfrica"))
AustraliaArea   =  createCountryClass("Australia",tr("Australia"))
ChileArea       =  createCountryClass("Chile",tr("Chile"))
ArgentinaArea   =  createCountryClass("Argentina",tr("Argentina"))
NewZealandArea  =  createCountryClass("NewZealand",tr("NewZealand"))

def newRegion(name,id,cepages=None):
    return name, id ,cepages

FranceArea().addRegions(
    "Burgundy",
    "Bordeaux",
    newRegion( tr("Alsace"), id="Alsace" , cepages=[ ["Pinot noir"],["Riesling", u"Gewürztraminer","Pinot gris"] ,[] ]),                                    
    "RhoneValley",
    "Beaujolais",
    "SouthWesternFrance",
    newRegion( tr("Champagne"), id="Champagne" ,cepages=[ ["Pinot noir"] , ["Chardonnay"],[]]),
    newRegion( tr("Jura"), id="Jura" ,cepages=[ ["Pinot noir","Trousseau"] , ["Chardonnay","Savagnin",],["Poulsard"]]),
    newRegion( tr("Savoy"), id="Savoy" , cepages=[ ["Gamay","Mondeuse"],[],[] ]), 
    newRegion( tr("Provence"), id="Provence" , cepages=[ ["Syrha",u"Mourvèdre","Grenache","Cinsault"],["Grenache blanc","Marsanne"],[] ]), 

    "LanguedocRoussillon",
    newRegion( tr("Corsica"), id="Corsica", cepages=[ ["Niellucio", "Sciacarello"],["Vermentino", ""],[] ]), 
    )
ItalyArea().addRegions(
    newRegion( tr("Piemont"), id="Piemont"  , cepages=[ ["Nebbiolo", ],[],[] ]), 
    newRegion( tr("Tuscany"), id="Tuscany"  , cepages=[ ["Sangiovese","Mammolo","Canaiolo" ],[],[] ]), 
    )


def testpickle():
    import pickle as pk
    w=sys.modules['wineAreas']
    b=wineWorld[u'France']['Bourgogne']
    b.makeWritable()
    setattr(w, b.__class__.__name__, b.__class__)
    pk.dump(b, open('bla.test','w'),-1 )
    

def translateToNew(a1,a2, i=0):
    print '--'*i,'translating ',a1,a2
    a2.name = a1.name
    a2._cepages = a1._cepages
    if isinstance(a1, _AppellationAreaMeths):
        print '**'*i,' done with ',a1.name, 'len ',len(a1._subAreas), len(a2._subAreas)
        return
    for sak,sa in a1._subAreas.iteritems():
        print '--'*i,'  willl add ', sak, sa
        sa2=a2.addSubArea(sa.name, groupApp = not isinstance(sa, _AppellationAreaMeths), addUserArea = False)        
        translateToNew(sa,sa2,i+1)
    print '**'*i,' done with ',a1.name, 'len ',len(a1._subAreas), len(a2._subAreas)

#
